﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Actors.Languages;
using System.Threading.Tasks;

namespace System.Threading.Actors.Utilities
{
    // Common logic for actor services
    public class ActorServiceHelper
    {
        private IActorState m_state;
        private string m_serviceName;
        private Action<IActorState> m_methodLoader;

        public ActorServiceHelper(IActorState state, string serviceName, Action<IActorState> methodLoader = null)
        {
            m_state = state;
            m_serviceName = serviceName;
            m_methodLoader = methodLoader;
            if (m_methodLoader == null) m_methodsInitialized = true;
        }

        // Called when deleting an actor.  Remove its state.
        public void Dispose()
        {
            m_state.UnsubscribeAll(null);
            ActorETWEventSource.Log.ActorDeleted(m_state.GetId(), m_state.PartitionName);
            m_state.Dispose();
            m_methodLoader = null;
        }

        private bool m_methodsInitialized = false;

        // Returns an ActorServiceResult encoded as a JSON string, ASR.Result is object
        public async Task<string> CallMethod(string clientId, int clientSequenceNumber, string methodName, string[] parameters)
        {
            Contract.Requires(clientId != null);

            // Make sure that any preLoadedMethods are loaded into our state.
            if (!m_methodsInitialized)
            {
                String possibleException = await PreloadMethodsAsync(clientSequenceNumber).ConfigureAwait(continueOnCapturedContext: false);
                if (possibleException != null)
                {
                    return possibleException;
                }
            }

            // OK, done with preLoadedMethods.  Let's move on to processing the specified method call.
            Task waiter = null;
            ActorServiceResult result = null;
            try
            {
                await m_state.AsyncStateLock.WaitAsync();
                //lock (m_state) // Ensure that only one client at a time has access to the stateProvider
                //{
                // Check to see whether this is a repeat request.  If it is, return the cached result from the original.
                object temp;
                bool clientKeyExists = false;
                if (clientSequenceNumber != ActorServiceRequest.FireAndForgetSequenceNumber && m_state.TryGet(clientId, out temp))
                {
                    var latest = temp as Tuple<int, object>;
                    if (latest != null)
                    {
                        clientKeyExists = true;
                        if (clientSequenceNumber == latest.Item1)
                        {
                            Console.WriteLine("Saw a duplicate from a client, returning previous result");
                            return ActorServiceResult.FromResult(clientSequenceNumber, latest.Item2).ToJson();
                        }
                        // TODO: What if seqNo < latest.Item1?
                    }
                    else
                    {
                        // If the client key exists, but is not a Tuple<int,object>, we have serious problems.
                        // That means that there has been a collision at the client key.  Throw an exception.
                        return ActorServiceResult.FromException(clientSequenceNumber, "Exception", 
                            String.Format("Internal: Collision at client unique id key {0}", clientId), Environment.StackTrace).ToJson();
                    }
                }

                try
                {
                    var objectArgs = new object[parameters.Length];
                    for (int index = 0; index < parameters.Length; index++)
                    {
                        objectArgs[index] = SerializationHelper.Deserialize(parameters[index]);
                    }

                    var objectResult = m_state.CallMethod(methodName, objectArgs);
                    result = ActorServiceResult.FromResult(clientSequenceNumber, objectResult);

                    ActorETWEventSource.Log.ActorMethodCompleted(m_serviceName, m_state.PartitionName, methodName, objectArgs, objectResult);

                    // Only update the client's sequence number if this operation had side effects
                    if (clientSequenceNumber != ActorServiceRequest.FireAndForgetSequenceNumber && m_state.ChangesPendingForCurrentOperation())
                    {
                        m_state.Set(clientId, Tuple.Create(clientSequenceNumber, objectResult));
                    }
                    else if (clientKeyExists)
                    {
                        // For now, implement "clean up" of client cookies, which are created during side-effecting operations,
                        // by deleting them during the next read-only operation.
                        m_state.Remove(clientId);
                    }
                }
                catch (Exception e)
                {
                    result = ActorServiceResult.FromException(clientSequenceNumber, e);
                    m_state.ClearChangesForCurrentOperation(); // Rollback

                    Exception baseException = (e is AggregateException) ? ((AggregateException)e).GetBaseException() : e;
                    ActorETWEventSource.Log.ActorMethodError(m_serviceName, m_state.PartitionName, methodName, parameters, // don't have converted parameters :(
                        baseException.GetType().Name, baseException.Message, baseException.StackTrace);
                        
                }

                //this.Logger.Write(Guid.Empty, "Actor about to flush");
                waiter = m_state.Flush(); // Commit, will be no-op if ClearChangesForCurrentOperation was called
            }
            finally { m_state.AsyncStateLock.Release(); }

            //this.Logger.Write(Guid.Empty, "Actor exited lock, about to begin waiter.Wait()");

            // It's possible that replication could fail in some fashion, so we need a try/catch here
            try
            {
                await waiter;
            }
            catch (Exception e)
            {
                var baseException = (e is AggregateException) ? ((AggregateException)e).GetBaseException() : e;
                // Let's reason here:
                //      If we had already failed due to an exception, then waiter.Wait() would not fail.
                //      So we had previously produced a good result, but now we failed to replicate.
                //      So our result now has to be an error.
                result = ActorServiceResult.FromException(clientSequenceNumber,
                                                          baseException.GetType().Name,
                                                          String.Format("Internal: ReplicationException: {0}", baseException.Message),
                                                          baseException.StackTrace);
            }
            //this.Logger.Write(Guid.Empty, "Actor done with waiter.Wait()");

            return result.ToJson();
        }

        // Loads all the relevant actor methods into the actor's state.
        // Returns null on success, or a non-null String on failure.
        private async Task<String> PreloadMethodsAsync(int clientSequenceNumber)
        {
            Task waiter = null;
            try
            {
                await m_state.AsyncStateLock.WaitAsync();
                if (!m_methodsInitialized)
                {
                    try
                    {
                        m_methodLoader(m_state);
                        waiter = m_state.Flush();
                    }
                    catch (Exception e)
                    {
                        ActorETWEventSource.Log.ActorMethodError(m_serviceName, m_state.PartitionName, "<ActorServiceHelper::PreloadMethods>", new Object[0],
                            e.GetType().Name, e.Message, e.StackTrace);

                        return ActorServiceResult.FromException(clientSequenceNumber,
                                                                e.GetType().Name,
                                                                String.Format("Internal: Failed Method Initialization/Flush().  Message = {0}", e.Message),
                                                                e.StackTrace).ToJson();
                    }
                }
            }
            finally { m_state.AsyncStateLock.Release(); }

            if (waiter != null)
            {
                try
                {
                    await waiter.ConfigureAwait(continueOnCapturedContext: false);
                    m_methodsInitialized = true;
                }
                catch (Exception e)
                {
                    var baseException = (e is AggregateException) ? ((AggregateException)e).GetBaseException() : e;
                    return ActorServiceResult.FromException(clientSequenceNumber,
                                                            baseException.GetType().Name,
                                                            String.Format("Internal: Failed to initialize methods.  Message = {0}", baseException.Message),
                                                            baseException.StackTrace).ToJson();
                }
            }
            return null;
        }

        // Returns an ActorServiceResult encoded as a JSON string, ASR.Result is bool
        // Allows a client to ship an assembly, possibly containing actor methods, to the actor.
        // Notes:
        // Assemblies are stored in the actor state in one of two ways:
        //      1. As an assembly, key=$ASSEMBLY$<assembly name>, value=Tuple<stringified assembly checksum, assembly byte array>
        //      2. As an assembly reference, key=$REF$ASSEMBLY$<assembly name>, value = checksum
        //
        // This routine first checks to see whether or not the *exact* assembly is already stored in the actor state,
        // either as an assembly or an assembly reference.  If it is not a duplicate, this assembly is then added to
        // the actor state, and the replication logic will "harvest" the actor methods from the assembly.
        public async Task<string> AddAssembly(int clientSequenceNumber, string assemblyName, byte[] assemblyBytes)
        {
            string keyName = AssemblyHelper.GetAssemblyKey(assemblyName); // the keyname for this assembly stored as an assembly
            string refKeyName = "$REF" + keyName; // the keyname for this assembly stored as an assembly reference
            bool result = true;

            // Compute the hash/checksum for this assembly.
            string hashString = Utilities.UtilityMethods.ComputeChecksumString(assemblyBytes);

            Task waiter = null;

            try
            {
                await m_state.AsyncStateLock.WaitAsync();
                //lock (m_state)
                //{
                Tuple<string, byte[]> storedAssemblyTuple;
                string storedHash;
                object temp;


                if (m_state.TryGet(keyName, out temp) &&
                    ((storedAssemblyTuple = temp as Tuple<string, byte[]>) != null) &&
                    storedAssemblyTuple.Item1.Equals(hashString))
                {
                    // We already have this exact assembly stored as an assembly.
                    ActorETWEventSource.Log.DuplicateAssemblyReceived(m_state.GetId(), m_state.PartitionName, assemblyName);
                }
                else if (m_state.TryGet(refKeyName, out temp) &&
                        ((storedHash = temp as string) != null) &&
                        storedHash.Equals(hashString))
                {
                    // We already have this exact assembly stored as an assembly reference.
                    ActorETWEventSource.Log.DuplicateAssemblyReceived(m_state.GetId(), m_state.PartitionName, assemblyName);
                }
                else
                {
                    // This is a brand new assembly (at least to this actor)
                    m_state.Set(keyName, Tuple.Create(hashString, assemblyBytes));
                    ActorETWEventSource.Log.AddAssembly(m_state.GetId(), m_state.PartitionName, assemblyName, "A"); // "A" for "Assembly"
                }

                // We always load the assembly, even if it means overwriting an old one
                result = true; // !stateProvider.TryGet(keyName, out objVal);  // Do we already have one of these?

                // Overwrite anyways.
                //m_state.Set(keyName, assemblyBytes);

                waiter = m_state.Flush();
            }
            finally { m_state.AsyncStateLock.Release(); }

            if (waiter != null) await waiter;

            return ActorServiceResult.FromResult(clientSequenceNumber, result).ToJson();
        }

        public async Task<string> AddMethod(int clientSequenceNumber, SupportedLanguages language, string methodName, string methodImpl)
        {
            Task waiter = null;
            string rval = null;
            try
            {
                if (!Enum.IsDefined(typeof(SupportedLanguages), language))
                    throw new ArgumentException("language not in valid range");

                try
                {
                    await m_state.AsyncStateLock.WaitAsync();
                    //lock (m_state)
                    //{
                    m_state.AddMethod(language, methodName, methodImpl);
                    waiter = m_state.Flush();
                }
                finally { m_state.AsyncStateLock.Release(); }

                if (waiter != null) await waiter;
                rval = ActorServiceResult.FromResult(clientSequenceNumber, true).ToJson();
            }
            catch (Exception e)
            {
                rval = ActorServiceResult.FromException(clientSequenceNumber, e).ToJson();
            }

            return rval;

        }

        public string Subscribe(int clientSequenceNumber, string eventType, IPublicationCallbackContract context)
        {
            // Pass the call on to the state provider
            m_state.Subscribe(context, eventType);
            ActorETWEventSource.Log.SubscriptionRequestReceived(m_state.GetId(), m_state.PartitionName, "<unknown>", eventType);
            return ActorServiceResult.FromResult(clientSequenceNumber, true).ToJson();
        }

        public string Unsubscribe(int clientSequenceNumber, string eventType, IPublicationCallbackContract context)
        {
            // Pass the call on to the state provider
            m_state.Unsubscribe(context, eventType);
            return ActorServiceResult.FromResult(clientSequenceNumber, true).ToJson();
        }

        public string UnsubscribeAll(int clientSequenceNumber, IPublicationCallbackContract context)
        {
            // Pass the call on to the state provider
            m_state.UnsubscribeAll(context);
            return ActorServiceResult.FromResult(clientSequenceNumber, true).ToJson();
        }

        public string Configure(int clientSequenceNumber, string configurationString)
        {
            string rval = null;
            try
            {
                m_state.Configure(configurationString);
                rval = ActorServiceResult.FromResult(clientSequenceNumber, null).ToJson();
            }
            catch (Exception e)
            {
                rval = ActorServiceResult.FromException(clientSequenceNumber, e).ToJson();
            }
            return rval;
        }

        public string DeleteSelf(int clientSequenceNumber)
        {
            Dispose();
            return ActorServiceResult.FromResult(clientSequenceNumber, true).ToJson();
        }
    }

}
